package com.yfs.cloud.controller;

import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.google.gson.Gson;
import com.yfs.cloud.commons.BusinessException;
import com.yfs.cloud.commons.Constants;
import com.yfs.cloud.commons.ResultCode;
import com.yfs.cloud.config.BusinessProperties;
import com.yfs.cloud.entity.User;
import com.yfs.cloud.service.SmsService;
import com.yfs.cloud.service.UserService;
import com.yfs.cloud.util.ApiIdempotentTokenUtil;
import com.yfs.cloud.util.BCryptUtil;
import com.yfs.cloud.util.CaptchaUtil;
import com.yfs.cloud.util.HttpClientUtil;
import com.yfs.cloud.util.JWTUtil;
import com.yfs.cloud.util.RedisUtil;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

/**
* @Company: 影法師工作室
* @Description: 登录控制器 , 处理用户登陆和用户注册逻辑, 支持 手机验证码登录或注册, 微信扫码登录, 用户名密码登录 
* @author 影法師  
* @date 2020年8月25日 上午7:26:35 
* @version V1.0
 */
@SuppressWarnings("all")
@RestController
@Api(description = "登录注册获取幂等令牌的接口")
@RequestMapping("/user/lr")
public class LoginController {

	@Autowired
	private UserService userService;

	@Autowired
	private JWTUtil jwtUtil;

	@Autowired
	private RedisUtil redisUtil;

	@Autowired
	private SmsService smsService;

	@Autowired
	private BusinessProperties businessProperties;

	@Autowired
	private ApiIdempotentTokenUtil apiIdempotentTokenUtil;

	@GetMapping("/getApiIdempotentToken")
	@ApiOperation("获取api幂等token,对于前端调用需要幂等的接口, 则前端在页面初始化的时候, 例如 vue页面的mounted时需要先调用此接口获取幂等token, 切记, 不是调用一次获取一次幂等token, 整个vue页面加载一次, 只获取一次幂等token")
	public String getApiIdempotentToken() {
		return apiIdempotentTokenUtil.getToken();
	}

	// ====================普通的用户名密码登录、注册=============================
	@PostMapping("/login")
	@ApiOperation("用户登录") // 据我所知, localstorage 可以清除某个 key, 所以jwt下的单点登出是再简单不过的了————只需要点击登出的时候,
							// 前端将localstorage清除即可(前端做而不是后端做, 因为后端是无状态的, 如果后端来做了的话, 那就是有状态的了)
	public String login(@RequestBody User user) throws Exception {
		User userQuery = new User();
		userQuery.setUsername(user.getUsername());
		// 根据账号查找唯一的用户
		User userInDB = userService.findOne(userQuery);
		// 如果没查到此用户
		Assert.notNull(userInDB, Constants.USER_NOT_EXIST);
		// 校验密码, 如果密码对不上
		if (!BCryptUtil.checkpw(user.getPassword(), userInDB.getPassword())) {
			throw new BusinessException(ResultCode.USER_LOGIN_FAILED);
		}
		// 填充进 jwt 中的声明
		Map<String, Object> param = new HashMap<>();
		// 仅仅放用户id就可以了, 以后要增加再说
		param.put(Constants.USER_ID, userInDB.getId());
		// 用户登录成功的话, 将生成 jwt 返回前端
		return jwtUtil.getJWT(param);
	}

	@PostMapping("/register")
	@ApiOperation("用户注册")
	public User register(@RequestBody @Validated User user) throws Exception {
		user.setPassword(BCryptUtil.encrypt(user.getPassword()));
		return userService.create(user);
	}

	// ====================手机验证码登录、注册=============================

	@GetMapping("/getCaptcha/{phone}")
	@ApiOperation("发送短信验证码")
	public void getCaptcha(@PathVariable("phone") String phone) {
		// 从redis获取验证码
		String code = (String) redisUtil.get(phone);
		// 如果不为空, 表示已经成功发送了短信, 则直接返回
		if (!StringUtils.isBlank(code)) {
			return;
		}
		// 如果redis获取不到, 则调用阿里云短信接口发送生成的随机验证码
		code = CaptchaUtil.getFourBitRandom();
		Map<String, Object> param = new HashMap<>();
		param.put("code", code);
		if (smsService.send(param, phone)) {
			// 发送成功，把发送成功验证码放到redis里面, 并设置有效时间(单位: 秒)
			redisUtil.set(phone, code, businessProperties.getSms().getExpire());
			return;
		} else {
			throw new BusinessException(ResultCode.SMS_SEND_FAILED);
		}
	}

	@GetMapping("/phonelr/{phone}/{captcha}") // 如果不用这种 @PathVariable 的方式的话, 就要在UserVO中再封装一个验证码属性,然后用 @RequestBody
												// UserVO userVO 接收入参, 这里图简单, 直接用Get请求了
	@ApiOperation("通过短信验证码进行登录或者注册合并在一起")
	public String phonelr(@PathVariable("phone") String phone, @PathVariable("captcha") String captcha)
			throws Exception {
		// 检查手机号码格式是否正确, 不正确的话, 后面查数据库、redis啥的都不必进行了
		if (StringUtils.isBlank(phone) || !Constants.PHONE_PATTERN.matcher(phone).matches()) {
			throw new BusinessException(ResultCode.PHONE_ERROR);
		}
		// 验证码需要非空
		Assert.notNull(captcha, Constants.CAPTCHA_ERROR);
		// 查询redis中相应的验证码, 注意, 先查redis再查数据库
		String code = (String) redisUtil.get(phone);
		// 如果用户填写的验证码错误的话, 抛出异常
		Assert.isTrue(captcha.equals(code), Constants.CAPTCHA_ERROR);
		// 数据库中根据手机号进行查询(注意, 手机号字段我们没做唯一索引, 也不需要, 因为这里会先查的)
		User userQuery = new User();
		userQuery.setPhone(phone);
		// 根据手机号查找唯一的用户
		User userInDB = userService.findOne(userQuery);
		// 如果没查到, 表明是用户注册
		if (Objects.isNull(userInDB)) {
			User user = new User();
			user.setPhone(phone);
			userInDB = userService.create(user);
			// 查到了, 表明是用户登录
		}
		// 填充进 jwt 中的声明, 即手机登录或者注册是合并在一起的, 只需要一次操作(填写手机号+验证码)就可以登录(不论你注册没)
		Map<String, Object> param = new HashMap<>();
		// 仅仅放用户id就可以了, 以后要增加再说
		param.put(Constants.USER_ID, userInDB.getId());
		// 用户登录成功的话, 将生成 jwt 返回前端, 注意, 为什么互联网项目不用 cookie 而使用 token? 因为 使用令牌轻松免去跨域的问题,
		// 而cookie 存在跨域的问题.
		return jwtUtil.getJWT(param);
	}

	/**
	* @Description: 这里的逻辑是这样的, 手机号登录注册(也就是 /phonelr)之后, 如果发现其实是新用户的话, 但是其实他曾经用户名密码注册过, 则就要引导手机号绑定该既存账户 , 这种绑定是可选的, 但是推荐用户做(所以要引导用户去做), 因为是一劳永逸的  
	* @author 影法師 
	* @date 2020年8月30日 上午8:33:09 
	* @version V1.0 
	* @param @param user    
	* @return void    
	* @throws Exception 
	 */
	@PostMapping("/bindPhoneAndLoginAccount")
	@ApiOperation("绑定手机号和既有登录账号")
	public void bindPhoneAndLoginAccount(@RequestBody User user) throws Exception {
		// 手机号
		String phone = user.getPhone();
		Assert.notNull(phone, Constants.PHONE_EMPTY);
		// 登录账号
		String username = user.getUsername();
		Assert.notNull(username, Constants.USERNAME_EMPTY);
		userService.modifyLeadByBindPhone(phone, username);
	}

	// ====================微信扫码登录、注册=============================
	// 生成微信扫码登录的二维码
	@GetMapping("/getWxQRCode")
	public String getWxQRCode() throws Exception {
		// 对redirectUrl进行URLEncoder编码
		String redirectUrl = businessProperties.getWxlogin().getRedirectUrl();
		redirectUrl = URLEncoder.encode(redirectUrl, "utf-8");
		// 设置%s里面值, 其中 state 没什么用, 随便写, 这是一个透传字段, 即会透传回 redirectUrl
		String url = String.format(Constants.WX_QR_CODE_GENERATE_URL, businessProperties.getWxlogin().getAppId(),
				redirectUrl, "yfs");
		// 返回给前端需要重定向到的那个属于微信的展示微信二维码的页面的URL(例如
		// https://open.weixin.qq.com/connect/qrconnect?appid=wxed9954c01bb89b47&redirect_uri=http%3A%2F%2Fguli.shop%2Fapi%2Fucenter%2Fwx%2Fcallback&response_type=code&scope=snsapi_login&state=yfs#wechat_redirect),
		// 用户手机扫描这个页面上的二维码,
		// 手机上就会询问是否允许第三方应用"我的谷粒"获取你的微信账号信息(昵称、头像、地区、性别), 然后如果你点击允许的话,
		// 则就会回调我们的redirectUrl这个接口, 我们在这个接口接收code(授权码),
		// 然后再通过httpClient向微信平台获取access_token, 最后通过access_token 获取用户的微信账号信息,
		// 这就是微信扫码登录的oauth2.0流程
		return url;
	}

	/**
	* @Description: 用户扫码之后, 重定向回本接口, 此接口就可以获取授权码code, 然后使用 HttpClientUtil 向微信通过code换令牌access_token, 再通过 access_token换用户微信账号, 这样就完成了微信扫码登录 
	* @author 影法師 
	* @date 2020年8月30日 上午11:17:24 
	* @version V1.0 
	* @param @param code 授权码, 非常重要, 类似于手机验证码
	* @param @param state 透传回的state字段
	* @param @return    
	* @return String    
	* @throws
	 */
	@GetMapping("/callback") // 需要将此接口联合公网域名暴露给微信平台
	public String callback(String code, String state) throws Exception {
		// 其实和上面手机+验证码的注册登录二合一的过程（即phonelr）极为相似. 都是二合一
		// 拿着code请求 微信固定的地址，得到两个值 accsess_token 和 openid, 需要拼接三个参数 ：id、 秘钥 和 code值
		String accessTokenUrl = String.format(Constants.WX_GET_ACCESS_TOKEN_URL,
				businessProperties.getWxlogin().getAppId(), businessProperties.getWxlogin().getAppsecret(), code);
		// 请求这个拼接好的地址，得到返回两个值 accsess_token 和 openid
		// 使用httpclient发送请求，得到返回结果, 得到的结果长 {"access_token":, "expire_in":
		// "refresh_token": , "openid", "scope", "unionid"} 这个样子
		// expire_in 默认是 2小时(7200秒), 表示你这个去获取用户微信账号信息的令牌2个小时内过期. openid 和 unionid的区别是
		// unionid是你的身份证号，openid是你的学号、工号啥的
		// openid只是用户在某一应用下的唯一标识，如A用户在小程序的openid与A用户在公众号的openid是不一致的,
		// 而同一用户，对同一个微信开放平台下的所有不同应用，unionid是相同的, openid一般用于应用的权限系统
		String accessTokenInfo = HttpClientUtil.get(accessTokenUrl);
		// 从accessTokenInfo字符串获取出来两个值 accsess_token(访问用户微信账号资源的令牌凭证) 和
		// openid(每个人的微信号都不一样, 微信名称可以一样, 比如都可以叫 "影法師", openid 却不同, openid 好比是主键,
		// 即openid是每个微信号在某一应用中的唯一表示)
		// 把accessTokenInfo字符串转换map集合，根据map里面key获取对应值
		// 这里使用json转换工具 Gson, 这也是微信扫码官方推荐的转换工具
		Gson gson = new Gson();
		HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
		String access_token = (String) mapAccessToken.get("access_token");
		String openid = (String) mapAccessToken.get("openid");
		// 根据 openid 查询唯一用户
		User userQuery = new User();
		userQuery.setOpenId(openid);
		User userInDB = userService.findOne(userQuery);
		// 说明是新用户注册
		if (Objects.isNull(userInDB)) {
			// 拿着得到accsess_token 和 openid，再去请求微信提供固定的地址，获取到扫描人信息
			String userInfoUrl = String.format(Constants.WX_GET_USERINFO_URL, access_token, openid);
			// 发送请求给微信平台, 取回用户的微信信息, 长 {openid: , nickname: sex: , language: city: ,
			// province: , country: , headimgurl: , privilege: , unionid: } 这个样子
			String userInfo = HttpClientUtil.get(userInfoUrl);
			// 获取返回userinfo字符串扫描人信息
			HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
			String nickname = (String) userInfoMap.get("nickname");// 昵称
			String headimgurl = (String) userInfoMap.get("headimgurl");// 头像
			userInDB = new User();
			// 填充微信信息
			userInDB.setOpenId(openid);
			userInDB.setWxNickName(nickname);
			userInDB.setWxAvatar(headimgurl);
			// 创建用户, 实现微信登录注册二合一
			userService.create(userInDB);
		}
		// 填充进 jwt 中的声明
		Map<String, Object> param = new HashMap<>();
		// 仅仅放用户id就可以了, 以后要增加再说
		param.put(Constants.USER_ID, userInDB.getId());
		// 用户登录成功的话, 将生成 jwt 返回前端
		return jwtUtil.getJWT(param);
	}

	/**
	* @Description: 和 上面 bindPhoneAndLoginAccount 的逻辑和应用场景是一样的
	* @author 影法師 
	* @date 2020年8月30日 上午8:33:09 
	* @version V1.0 
	* @param @param user    
	* @return void    
	* @throws Exception 
	 */
	@PostMapping("/bindWxAndLoginAccount")
	@ApiOperation("绑定微信和既有登录账号")
	public void bindWxAndLoginAccount(@RequestBody User user) throws Exception {
		// 微信openid
		String openid = user.getOpenId();
		Assert.notNull(openid, Constants.WX_OPENID_EMPTY);
		// 登录账号
		String username = user.getUsername();
		Assert.notNull(username, Constants.USERNAME_EMPTY);
		userService.modifyLeadByBindWx(openid, username);
	}

}
